//
//  HTTPServer.java
//  SketchServer
//
//  Created by Brian Hinch on 9/11/08.
//  Copyright 2008 Tellart. All rights reserved.
//

import java.lang.reflect.*;
import java.util.*;
import java.io.*;
import java.nio.charset.*;
import java.security.*;
import java.net.*;
import javax.net.*;
import javax.net.ssl.*;
import java.security.*;
import java.security.cert.*;
import javax.swing.*;

public class SketchUpdateController {

    public static final int TCP_PORT = 8080;
    private static final int MAX_CONCURRENT_CONNECTIONS = 10;
    private static final int IO_LOOP_DELAY = 1;

    private boolean started = false;
    private boolean connected = false;
    
    private ServiceAdvertiser advertiser;
    
    private String interfaceName;
	private ServerSocket sSocket;		//Main server socket
    private Socket cSocket;

    private MyServer server;
	
	protected static final String GLOBAL_CHARACTER_ENCODING	= "UTF-8";
	protected static final Charset GLOBAL_CHARSET;
	static {
		GLOBAL_CHARSET = Charset.forName(GLOBAL_CHARACTER_ENCODING);
	}

	protected static final String HTTP_PROTOCOL_1_0			= "HTTP/1.0";
	protected static final String HTTP_PROTOCOL_1_1			= "HTTP/1.1";
	protected static final String HTTP_METHOD_GET			= "GET";
	protected static final String HTTP_METHOD_PUT			= "PUT";
	protected static final String HEADER_CONTENT_ENCODING	= "Content-Encoding";
	protected static final String HEADER_CONTENT_TYPE		= "Content-Type";
	protected static final String HEADER_CONTENT_LENGTH		= "Content-Length";
	protected static final String URI_PATH_SEPARATOR		= "/";

	protected static final String DEFAULT_PAGE_URI_PATH             = URI_PATH_SEPARATOR + SketchFileFilter.PROJECT_INDEX_FILE_NAME;
	protected static final String SKETCH_UPDATE_ROOT_URL_PATH		= "/sketches";
	protected static final String SKETCH_UPDATE_BASE_DIR			= "sketch";
	protected static final String SKETCH_UPDATE_BASE_URL_PATH		= SKETCH_UPDATE_ROOT_URL_PATH + "/" + SKETCH_UPDATE_BASE_DIR;
    protected static final String SKETCH_UPDATE_CATALOG_URL_PATH	= SKETCH_UPDATE_ROOT_URL_PATH + "/index.php";
    protected static final String SKETCH_UPDATE_CAPABILITIES_URL_PATH	= SKETCH_UPDATE_ROOT_URL_PATH + "/caps.php";
	protected static final String DASHCODE_SUBDIRECTORY				= "/project";
	protected static final String DASHCODE_TARGET_SUBDIRECTORY		= "/mobile";
    protected static final String DASHCODE_PATH_SEPARATOR           = "/";
    
    /*************************************************************************/
    
    private String documentRootPath = null;
    private String fileChooserRootPath = null;
    
    public SketchUpdateController() {
    } //constructor
    
    /*
     * starts the controller
     *
     */
    public void start(String ifc) throws AlreadyStartedException, UnknownHostException, IOException  {
        if (started) throw new AlreadyStartedException();
        interfaceName = ifc;
        InetAddress addr = openPort();
        
        started = true;
        
        //start advertising over jmdns
        advertiser = new ServiceAdvertiser(
            addr,
            "nmsu",
            "tcp",
            TCP_PORT,
            "NADA Mobile sketch update server"
        );
        advertiser.start();
        
        //start the actual server
        server = new MyServer();
        server.start();
        
    } //start
    
    /*
     * stops the controller
     *
     */
    public void stop() {
        if (!started) return;
        started = false;
        connected = false;

        //stop advertising
        advertiser.stop();

        //shut down
        closePort();

        try {
            server.interrupt();
        } catch (Exception e) {
            System.err.println("Exception: " + e.getLocalizedMessage());
            e.printStackTrace(System.err);
        }
    }
    
    
    public boolean isStarted() {
        return started;
    } //isStarted
    
    
    public boolean isConnected() {
        return connected;
    } //isConnected
    
        
    private InetAddress openPort() throws UnknownHostException, IOException {
		
		InetAddress inetAddress = InetAddress.getByName(interfaceName);
        sSocket = ServerSocketFactory.getDefault().createServerSocket(TCP_PORT, MAX_CONCURRENT_CONNECTIONS, inetAddress);
        //sSocket.setSoTimeout(5000);
        return inetAddress;
    } //openPort
    
    private void closePort() {
		System.out.println("SketchUpdateController: closing port");
		if (sSocket == null) return;
		try {
			sSocket.close();
            cSocket.close();
		} catch (Exception e) { }
		sSocket = null;
    } //closePort

    /************************************************************************/
    
    public void setDocumentRootPath(String p) {
        documentRootPath = p;
    } //setDocumentRootPath
    

	/**
	 * processes an HTTP request, returns null if bad request or an array if a good one:
	 *
	 *	0: request method
	 *	1: request URI
	 *	2: request protocol
	 *
	 * This method blocks until the request is complete!
	 *
	 */
	static String[] httpProcessRequest(Reader streamIn) throws IOException {
		return httpProcessRequest(new BufferedReader(streamIn));
	}
	static String[] httpProcessRequest(BufferedReader streamIn) throws IOException {
		boolean badRequest = false;
		String requestLine = null;
		String[] parts = null;

		/* System.out.println("[" + fragment + "]");
		do {
			requestLine = streamIn.readLine();
			System.out.println(requestLine);
		} while (requestLine != null);
		
		return new String[0]; */

		requestLine = streamIn.readLine();
		/*
		if (requestLine == null) {
			requestLine = fragment;
		} else {
			requestLine = new StringBuffer().append(fragment).append(requestLine).toString();
		}
		*/

		if ((requestLine != null) && !requestLine.equals("")) {
			System.out.println("HTTPServer: INCOMING REQUEST: " + requestLine);
			
			parts = requestLine.split(" ", 3);
			if (parts.length < 2) {
				parts = null;
				
			} else if (parts.length < 3) {
				parts = new String[] {
					parts[0],
					parts[1],
					HTTP_PROTOCOL_1_0
				};
			}
			
		} else {
			parts = null;
			
		} //if not a null line input
		
		return parts;
		
	} //processRequest


	/**
	 * writes a short HTTP response to the specified outputstream
	 *
	 */
	static void httpRespond(Writer outWriter, String responseCode, String responseReason, String responseContentType, String responseBody) throws IOException {
		httpResponseOpen(outWriter, responseCode, responseReason, responseContentType, responseBody.length());
		httpResponseWrite(outWriter, responseBody);
		httpResponseClose(outWriter);

	} //respond
	
	
	/**
	 * begins an HTTP response by sending headers
	 *
	 */
	static void httpResponseOpen(Writer outWriter, String responseCode, String responseReason, String responseContentType) throws IOException {
		outWriter.write(HTTP_PROTOCOL_1_1 + " " + responseCode + " " + responseReason + "\r\n");
		
		//Send some headers and then the body
		outWriter.write(HEADER_CONTENT_ENCODING + ": UTF-8\r\n");
		if (responseContentType != null) outWriter.write(HEADER_CONTENT_TYPE + ": " + responseContentType + "\r\n");
		//streamOut.writeBytes(HEADER_CONTENT_LENGTH + ": " + responseContentLength + "\r\n");
		outWriter.write("\r\n");
		outWriter.flush();
		
	} //httpResponseOpen
	/**
	 * begins an HTTP response by sending headers
	 *
	 */
	static void httpResponseOpen(Writer outWriter, String responseCode, String responseReason, String responseContentType, long responseContentLength) throws IOException {
		outWriter.write(HTTP_PROTOCOL_1_1 + " " + responseCode + " " + responseReason + "\r\n");
		
		//Send some headers and then the body
		outWriter.write(HEADER_CONTENT_ENCODING + ": UTF-8\r\n");
		if (responseContentType != null) outWriter.write(HEADER_CONTENT_TYPE + ": " + responseContentType + "\r\n");
		outWriter.write(HEADER_CONTENT_LENGTH + ": " + responseContentLength + "\r\n");
		outWriter.write("\r\n");
		outWriter.flush();
		
	} //httpResponseOpen
	
	
	/**
	 * writes arbitrary data as part of an HTTP response
	 *
	 */
	static void httpResponseWrite(Writer outWriter, String responseBodyFragment) throws IOException {
		outWriter.write(responseBodyFragment);
		outWriter.flush();
	} //httpResponseWrite
	static void httpResponseWrite(Writer outWriter, char[] cbuf, int offset, int len) throws IOException {
		outWriter.write(new String(cbuf, offset, len));
		outWriter.flush();
	} //httpResponseWrite
	/* static void httpResponseWrite(Writer outWriter, byte[] cbuf, int offset, int len) throws IOException {
		outWriter.write(cbuf, offset, len);
		outWriter.flush();
	} */ //httpResponseWrite
	
	
	/**
	 * closes an HTTP response
	 *
	 */
	static void httpResponseClose(Writer outWriter) throws IOException {
		//streamOut.writeBytes(outBuffer.toString());
		outWriter.flush();
		//System.out.println("HTTP RESPONSE: " + outBuffer.toString());
		
	} //httpResponseClose
	
	
	/**
	 * parses URI requests for self-documentation of the server from HTTP clients
	 *
	 */
	void httpServeDocs(DataOutputStream streamOut, String[] request) {
	
		long serviceStartMS = System.currentTimeMillis();
	
		String responseCode = "404";
		String responseReason = "Not Found";
		String contentType = "text/html";
		String responseBody = "";
		
		if ((request.length >= 3) && request[0].equals(HTTP_METHOD_GET)) {
			String requestMethod = request[0];
			URI requestURI = null;
			try {
				requestURI = new URI(request[1]);
			} catch (Exception e) { }
			String requestProtocol = request[2];
			
			//filter out path attacks - URI must not contain ".."
			if ((requestURI != null) && (requestURI.getPath().indexOf("..") < 0)) {
				System.out.println("SketchUpdateController.httpServeDocs: GET " + requestURI.toString());
				
				//prepare to handle the request
				InputStream docIn = null;
				
				//handle any "special" paths
				
				if (requestURI.getPath().equals(SKETCH_UPDATE_CATALOG_URL_PATH)) {
					//request for catalog
					chooseSketchLocation();
					if (documentRootPath != null) {
						responseCode = "200";
						responseReason = "OK";
						responseBody = getCatalogXMLForDocumentRootPath();
					}

                } else if (requestURI.getPath().equals(SKETCH_UPDATE_CAPABILITIES_URL_PATH)) {
						responseCode = "200";
						responseReason = "OK";
						responseBody = "<capabilities sketchserver=\"true\" iopack=\"true\"></capabilities>";

				} else {
					//request for a specific document
					
					//try to find a resource for the server's locale
					//URL uri = null;
					if ((requestURI != null) && requestURI.getPath().startsWith(SKETCH_UPDATE_BASE_URL_PATH)) {
						try {
							String p = documentRootPath + requestURI.getPath().substring(SKETCH_UPDATE_BASE_URL_PATH.length());
							System.out.println("SketchUpdateController.httpServeDocs: mapped " + requestURI.getPath() + " to " + p);
							docIn = new FileInputStream(new File(p));
						} catch (Exception e) {
							System.err.println("HTTPServer: Error opening requested resource \"" + request[1] + "\": " + e.getLocalizedMessage());
							e.printStackTrace(System.err);
						}
					}
				}
				
				//if the request resolves to an actual document on disk, docIn will point to the document
				if (docIn != null) {
					//serve it now
					try {

						responseCode = "200";
						responseReason = "OK";
						contentType = getContentType(requestURI);
						
						Writer outWriter = null;
						try {
							outWriter = new BufferedWriter(new OutputStreamWriter(streamOut));
						} catch (Exception e) { }
						
						if (outWriter != null) {
                            //don't parse this file; just spit the file to the output stream
                            
                            httpResponseOpen(outWriter, responseCode, responseReason, contentType);
                            int bufLen = 10240;
                            byte[] cbuf = new byte[bufLen];
                            int bytesRead = 0;
                            while (bytesRead > -1) {
                                bytesRead = docIn.read(cbuf, 0, bufLen);
                                //if (bytesRead > -1) httpResponseWrite(outWriter, cbuf, 0, bytesRead); //doc.append(cbuf, 0, len);
                                //debugOut("Bytes read: " + bytesRead);
                                if (bytesRead > -1) streamOut.write(cbuf, 0, bytesRead);
                            }
                            httpResponseClose(outWriter);
                            streamOut.flush();
						} //if outWriter != null
						
						docIn.close();
						
						System.out.println("HTTPServer: Total time from HTTP request to connection close: " + (System.currentTimeMillis() - serviceStartMS) + "ms");
						
						return;
						
					} catch (Exception e) {
						System.err.println("HTTPServer: ERROR " + e.getMessage());
						e.printStackTrace(System.err);
						
						responseCode = "500";
						responseReason = "Internal Server Error";
						contentType = "text/html";
						responseBody = "";
						
					} finally {
						try {
							docIn.close();
						} catch (Exception e) { }
					}
					
				} //if not null URI
				
			} else { //if valid URI
				System.err.println("HTTPServer: Illegal resource: \"" + request[1] + "\"");
			}

		} else { //if valid request
			System.err.println("HTTPServer: Malformed HTTP request from client");
			
			responseCode = "400";
			responseReason = "Bad Request";
			contentType = "text/html";
			responseBody = "";
		}
	
		try {
			Writer outWriter = new BufferedWriter(new OutputStreamWriter(streamOut, GLOBAL_CHARSET));
			httpRespond(outWriter, responseCode, responseReason, contentType, responseBody);
			
		} catch (Exception e) {
			e.printStackTrace(); 
		}
	
	} //serveDocs
	
	
	/**
	 * returns the MIME content type for the resource indicated by the given URI
	 *
	 * @param uri The URI to get the type of
	 *
	 */
	String getContentType(URI uri) {
		String p = uri.getPath().toLowerCase();
		if (p.endsWith(".gif")) {
			return "image/gif";
		} if (p.endsWith(".jpg")) {
			return "image/jpeg";
		} else if (p.endsWith(".html") || p.endsWith(".htm") || p.endsWith(".dsp")) {
			return "text/html";
		} else if (p.endsWith(".txt") || p.endsWith(".java") || p.endsWith(".as")) {
			return "text/plain";
		} else if (p.endsWith(".css")) {
			return "text/css";
		} else {
			return "application/octet-stream";
		}
	} //getContentType
	
	private void chooseSketchLocation() {
		JFileChooser chooser = new JFileChooser();
		if (documentRootPath != null) {
			chooser.setCurrentDirectory(new File(fileChooserRootPath).getParentFile());
			chooser.setSelectedFile(new File(fileChooserRootPath));
		}
		chooser.setDialogTitle("Open Sketch");
		chooser.setFileFilter(SketchFileFilter.getSwingFilter());
		//chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
			//chose a directory
			if (SketchFileFilter.isDashcodeProject(chooser.getSelectedFile())) {
				//is a dashcode project
                
                File docRootCandidate = chooser.getSelectedFile();
                File index = new File(docRootCandidate.getPath() + DASHCODE_SUBDIRECTORY + DASHCODE_PATH_SEPARATOR + SketchFileFilter.PROJECT_INDEX_FILE_NAME);
                
                System.out.println("SketchUpdateController.chooseSketchLocation: Checking index - "+index);
                if (index.exists() && index.isFile()) {
                    //found index in project/index
                    setDocumentRootPath(docRootCandidate.getPath() + DASHCODE_SUBDIRECTORY);
                    System.out.println("SketchUpdateController.chooseSketchLocation: found project/index");
                    
                } else {
                    File targetDir = new File(docRootCandidate.getPath() + DASHCODE_SUBDIRECTORY + DASHCODE_TARGET_SUBDIRECTORY);
                    System.out.println("SketchUpdateController.chooseSketchLocation: Checking target - "+targetDir);
                    if (targetDir.exists() && targetDir.isDirectory()) {
                        index = new File(targetDir, SketchFileFilter.PROJECT_INDEX_FILE_NAME);
                        System.out.println("SketchUpdateController.chooseSketchLocation: Checking index - "+index);
                        if (index.exists() && index.isFile()) {
                            //found index in project/mobile/index
                            setDocumentRootPath(docRootCandidate.getPath() + DASHCODE_SUBDIRECTORY);
                            System.out.println("SketchUpdateController.chooseSketchLocation: found index/mobile/index");
                        } else {
                            //found project/mobile, but no index
                            setDocumentRootPath(null);
                            System.out.println("SketchUpdateController.chooseSketchLocation: found project/mobile, but no index");
                        }
                    } else {
                        //no index in project root
                        setDocumentRootPath(null);
                        System.out.println("SketchUpdateController.chooseSketchLocation: found root, but no index");
                    }
                }

                fileChooserRootPath = docRootCandidate.getPath();
                
            } else if (SketchFileFilter.isProjectIndex(chooser.getSelectedFile())) {
                //is a project index
                setDocumentRootPath(chooser.getSelectedFile().getParentFile().getPath());
			} else {
				//is just a regular directory
				setDocumentRootPath(chooser.getSelectedFile().getPath());
			}
		} else {
			setDocumentRootPath(null);
		}
	} //chooseSketchLocation
	
	private String getCatalogXMLForDocumentRootPath() {
		if (documentRootPath == null) return null;

		Integer totalSize = new Integer(0);
		Integer numFiles = new Integer(0);
		
		//loop through the tree located at documentRootPath and add lines to the buffer for each
		File src = new File(documentRootPath);
		StringBuffer sb = catalogMapDir(documentRootPath, "", new File(documentRootPath), totalSize, numFiles);

		return	"<catalog base=\"" + SKETCH_UPDATE_BASE_URL_PATH + "\" size=\""+totalSize.toString()+"\" files=\""+numFiles.toString()+"\">\n" +
				sb.toString() +
				"</catalog>\n";
		
	} //getCatalogXMLForDocumentRootPath
	
	private StringBuffer catalogMapDir(String rootPath, String rootURL, File srcParent, Integer totalSize, Integer numFiles) {
		String path;
		int size = 0;
		StringBuffer sb = new StringBuffer();
		File[] contents = srcParent.listFiles();
		
		for (int f = 0; f < contents.length; f++) {
			//check permissions
			if (!contents[f].canRead()) {
				System.err.println("SketchUpdateController.catalogMapDir: Cannot read source file \"" + contents[f].getPath() + "\", skipping.");

			} else if (contents[f].isDirectory()) {
				/** directory **/
				//add an entry
				path = contents[f].getPath().substring(rootPath.length());
				sb.append("<asset type=\"dir\" src=\"" + rootURL + path + "\" />\n");
				//recurse
				sb.append(catalogMapDir(rootPath, rootURL, contents[f], totalSize, numFiles));
				
			} else if (contents[f].isFile()) {
				/** regular file **/
				//add an entry
				//size = FileSystem.getFileSystem().getLength(contents[f]);
				path = contents[f].getPath().substring(rootPath.length());
				sb.append("<asset type=\"file\" src=\"" + rootURL + path + "\" size=\"" + size + "\" />\n");
				totalSize = new Integer(totalSize.intValue() + size);
				numFiles = new Integer(numFiles.intValue() + 1);
				
			} else {
				/** not a directory OR a regular file... what is it?? **/
				System.err.println("SketchUpdateController.catalogMapDir: Found special filesystem object \"" + contents[f].getPath() + "\", skipping.");
			}
		}
		
		return sb;
		
	} //catalogMapDir
	

    private class MyServer extends Thread {
        public MyServer() {
            super("SketchUpdateController.MyServer");
        }
        
        public void run() {
            while (started) {
                try {
                    System.out.println("SketchUpdateController.MyServer.run: waiting for a connection... on "+interfaceName);
                    System.out.println("SketchUpdateController.MyServer.run: "+sSocket);
                    
                    //block until a client connects to the server
                    cSocket = sSocket.accept();
                    System.out.println("SketchUpdateController.MyServer: Conencted");
                    connected = true;
                    
                    //set up the IO streams
					InputStreamReader r = new InputStreamReader(cSocket.getInputStream());
					BufferedReader streamIn = new BufferedReader(r);
					DataOutputStream dStreamOut = new DataOutputStream(cSocket.getOutputStream());
					httpServeDocs(dStreamOut, httpProcessRequest(streamIn));

					//after disconnection we have to open the port again for some reason
					closePort();
					openPort();
                    
                } catch (Exception e) {
                    System.err.println("SketchUpdateController.MyServer: Exception: " + e.getLocalizedMessage());
                    e.printStackTrace(System.err);
                }
                System.out.println("SketchUpdateController.MyServer: Disconnected");
                connected = false;

            } //while started
            System.out.println("SketchUpdateController.MyServer: Stopped");
                
        } //run
    } //inner class

} //SketchUpdateController class

